/*
    之后的驱动开发就是用这个led灯的开发为基础
*/
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>
#include <linux/err.h>

/* 这是一个驱动开发板上led灯的设备驱动 */
#define LED_CNT 1      /* 设备号个数 */
#define LED_NAME "led" /* 设备名称 */
#define LED_OFF 0      /* 关灯 */
#define LED_ON 1       /* 开灯 */

/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)                       // 时钟控制寄存器地址
#define IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04_BASE (0x20E006C) // gpio复用功能选择寄存器(都是gpio_0104这个引脚)
#define IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04_BASE (0x20E02F8) // PAD属性设置寄存器
#define GPIO1_GDIR_BASE (0x0209C004)                      // gpio方向设置寄存器
#define GPIO1_DR_BASE (0x0209C000)                        // gpio输出状态寄存器

/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1;
static void __iomem *SW_MUX_GPIO1_IO04;
static void __iomem *SW_PAD_GPIO1_IO04;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;

/* led设备结构体 */
struct led_dev
{
    dev_t devid;           // 设备号
    struct cdev cdev;      // cdev
    struct class *class;   // 类
    struct device *device; // 设备
    int major;             // 主设备号
    int minor;             // 从设备号
};

struct led_dev led; // led设备

// led开关函数
void led_switch(u8 sta)
{
    u32 val = 0;
    if (sta == LED_ON)
    {
        val = readl(GPIO1_DR);
        val &= ~(1 << 3);
        writel(val, GPIO1_DR);
    }
    else if (sta == LED_OFF)
    {
        val = readl(GPIO1_DR);
        val |= (1 << 3);
        writel(val, GPIO1_DR);
    }
}

/*
    打开设备
*/
static int led_open(struct inode *inode, struct file *filp)
{
    filp->private_data = &led;
    return 0;
}

/*
    从设备读取数据
*/
static ssize_t led_read(struct file *filp, char __user *buf, size_t cnt, loff_t *offt)
{
    return 0;
}

/*
    向设备写入一个数据
*/
static ssize_t led_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
    int retvalue;
    unsigned char databuf[1];
    unsigned char ledstat;

    /*
    函数：copy_from_user    :从用户区拷贝数据到内核区
    第一个参数：void * to   :代表用户区的数据要拷贝到
    第二个参数: void __user * from :代表数据从用户区拷贝过来的存储地址
    */
    retvalue = copy_from_user(databuf, buf, cnt);
    if (retvalue < 0)
    {
        printk("kernel write falied\r\n"); // 返回值小于0就代表copy十八，内核写入失败
        return -EFAULT;
    }
    ledstat = databuf[0];
    if (ledstat == LED_ON)
    {
        led_switch(LED_ON);
    }
    else if (ledstat == LED_OFF)
    {
        led_switch(LED_OFF);
    }
    return 0;
}

/* 关闭设备 */
static int led_release(struct inode *inode, struct file *filp)
{
    return 0;
}

/* 设备操作函数 */
static struct file_operations LedDev_fops = {
    .owner = THIS_MODULE,
    .open = led_open,
    .read = led_read,
    .write = led_write,
    .release = led_release,
};

/* 驱动入口函数 */
static int __init led_init(void)
{
    u32 val = 0;
    // 先进行内存地质映射
    IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
    SW_MUX_GPIO1_IO04 = ioremap(IOMUXC_SW_MUX_CTL_PAD_GPIO1_IO04_BASE, 4);
    SW_PAD_GPIO1_IO04 = ioremap(IOMUXC_SW_PAD_CTL_PAD_GPIO1_IO04_BASE, 4);
    GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
    GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);

    // 使能gpio时钟
    val = readl(IMX6U_CCM_CCGR1); // 获取地址中存储的值,执行这个函数之后vol就是寄存器的地址了
    val &= ~(3 << 26);
    val |= (3 << 26);
    writel(val, IMX6U_CCM_CCGR1); // 向寄存器地址中写入值

    // 设置gpio的复用功能
    writel(5, SW_MUX_GPIO1_IO04);

    /*寄存器SW_PAD_GPIO1_IO03设置IO属性
     *bit 16:0 HYS关闭
     *bit [15:14]: 00 默认下拉
     *bit [13]: 0 kepper功能
     *bit [12]: 1 pull/keeper使能
     *bit [11]: 0 关闭开路输出
     *bit [7:6]: 10 速度100Mhz
     *bit [5:3]: 110 R0/6驱动能力
     *bit [0]: 0 低转换率
     */
    writel(0x10B0, SW_PAD_GPIO1_IO04);

    /* 4、设置GPIO1_IO04为输出功能 */
    val = readl(GPIO1_GDIR);
    val &= ~(1 << 3); /* 清除以前的设置 */
    val |= (1 << 3);  /* 设置为输出 */
    writel(val, GPIO1_GDIR);

    /* 5、默认关闭LED */
    val = readl(GPIO1_DR);
    val |= (1 << 3);
    writel(val, GPIO1_DR);

    /*
    最重要的部分：自动创建设备节点和写在设备节点，自动申请主从设备号
    设备机构体：led
    */
    if (led.major)
    {
        led.devid = MKDEV(led.major, 0);
        register_chrdev_region(led.devid, LED_CNT, LED_NAME);
    }
    else
    {
        alloc_chrdev_region(&led.devid, 0, LED_CNT, LED_NAME); // 申请设备号
        led.major = MAJOR(led.devid);                          // 获取申请到的主设备号
        led.minor = MINOR(led.devid);                          // 获取申请到的从设备号
    }
    printk("led major =%d ,minor =%d", led.major, led.minor);

    // 初始化cdev
    led.cdev.owner = THIS_MODULE;
    cdev_init(&led.cdev, &LedDev_fops);

    // 添加一个cdev
    cdev_add(&led.cdev, led.devid, LED_CNT);

    // 创建类
    led.class = class_create(THIS_MODULE, LED_NAME);
    if (IS_ERR(led.class))
    {
        return PTR_ERR(led.class);
    }

    // 创建设备
    led.device = device_create(led.class, NULL, led.devid, NULL, LED_NAME);
    if (IS_ERR(led.device))
    {
        return PTR_ERR(led.device);
    }
    return 0;
}

// 驱动出口函数
static void __exit led_exit(void)
{
    // 取消映射
    iounmap(IMX6U_CCM_CCGR1);
    iounmap(SW_MUX_GPIO1_IO04);
    iounmap(SW_PAD_GPIO1_IO04);
    iounmap(GPIO1_DR);
    iounmap(GPIO1_GDIR);

    // 注销字符设备驱动
    cdev_del(&led.cdev);
    unregister_chrdev_region(led.devid, LED_CNT);
    device_destroy(led.class, led.devid);
    class_destroy(led.class);
}

module_init(led_init);
module_exit(led_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("hanyang");
